/*****************************************************************************

Copyright (c) 2006 Nokia Corporation. All rights reserved.

This software is proprietary to and embodies the confidential technology
of Nokia. Possession, use, or copying of this software and media is
authorized only pursuant to a valid written license from Nokia.

Notes :
*****************************************************************************/

package com.kaixin001.tk.util;


import java.text.MessageFormat;
import java.util.PriorityQueue;

import com.kaixin001.tk.api.Scheduler;

/**
 * Implementation of scheduler.
 * @author Dmitry Mikhailov
 */
public class SchedulerImpl
    extends Thread
    implements Scheduler
{
    /**
     * Inner class for wrapping tasks.<br>
     * Synchronization is done through main class.
     * @author Dmitry Mikhailov
     */
    private class Task
        implements Runnable, Comparable<Task>
    {
        /** Base time */
        private long m_baseTime = 0;

        /** Is execution in progress? */
        private boolean m_isRunning = false;

        /** Time for next scheduled run */
        private long m_nextRunTime = 0;

        /** Period */
        private long m_period = 0;

        /** Should skip concurrent executions */
        private final boolean m_skipConcurrent;

        /** Actual task to be executed */
        private final Runnable m_task;

        /**
         * Constructor.
         * @param task actual task to execute
         * @param skipConcurrent should skip concurrent executions?
         */
        public Task(Runnable task, boolean skipConcurrent)
        {
            m_task = task;
            m_skipConcurrent = skipConcurrent;
        }

        /**
         * @see java.lang.Comparable#compareTo(java.lang.Object)
         */
        public int compareTo(Task o)
        {
            int r = (int) (m_nextRunTime - o.m_nextRunTime);

            if (r == 0)
            {
                r = System.identityHashCode(this) - System.identityHashCode(o);
            }

            return r;
        }

        /**
         * Changes scheduling of this task.
         * @param base new base time
         * @param period new base period
         */
        public final void reschedule(long base, long period)
        {
            synchronized (SchedulerImpl.this)
            {
                boolean wasCanceled = (m_period == 0);

                m_baseTime = base;
                m_period = period;

                if (m_isRunning)
                {
                    // will reschedule after run is finished
                    return;
                }

                if (!wasCanceled)
                {
                    cancelNextRun(this);
                }

                if (m_period == 0)
                {
                    return;
                }

                if (m_isClosed)
                {
                    throw new IllegalStateException("Scheduler is closed");
                }

                m_nextRunTime = getNextRunTime(System.currentTimeMillis(),
                        m_baseTime, m_period);
                scheduleNextRun(this);
            }
        }

        /**
         * @see java.lang.Runnable#run()
         */
        public void run()
        {
            try
            {
                m_task.run();
            }
            catch (Throwable thr)
            {
            }

            endRun();
        }

        /**
         * Ends run and schedules next execution.
         */
        final void endRun()
        {
            synchronized (SchedulerImpl.this)
            {
                m_isRunning = false;

                if (m_isClosed || (m_period <= 0))
                {
                    return;
                }

                long prevTime = m_nextRunTime;
                long currTime = System.currentTimeMillis();

                m_nextRunTime = getNextRunTime(currTime, m_baseTime, m_period);
                if (!m_skipConcurrent && (m_nextRunTime - prevTime) > m_period
                    && (m_nextRunTime != m_baseTime))
                {
                    m_nextRunTime = currTime;
                }

                scheduleNextRun(this);
            }
        }

        /**
         * Starts new run.
         */
        final void startRun()
        {
            m_isRunning = true;
        }

    }


    /**
     * Returns time for next run
     * @param currTime current time
     * @param baseTime base time for a task
     * @param period periof for a task
     * @return time for next run
     */
    public static long getNextRunTime(long currTime, long baseTime, long period)
    {
        if (currTime < baseTime)
        {
            return baseTime;
        }

        return (currTime - (currTime - baseTime) % period + period);
    }


    /** "Is closed" flag */
    private boolean m_isClosed = false;

    /** Tasks queue */
    private final PriorityQueue<Task> m_tasks = new PriorityQueue<Task>();

    /**
     * Constructor.
     * @param name instance name
     * @param executor executor to use
     */
    public SchedulerImpl(String name)
    {
        super(name);
        //setDaemon(true);
        start();
    }

    /**
     * @see com.kaixin001.tk.api.Scheduler#cancelTask(java.lang.Object)
     */
    public void cancelTask(Object id)
    {
        if (!(id instanceof Task))
        {
            throw new IllegalArgumentException(
                MessageFormat.format("Invalid task id {0}", id));
        }

        Task t = (Task) id;

        t.reschedule(0, 0);
    }

    /**
     * Stops this scheduler and all scheduled tasks. Scheduler cannot be reused
     * later. All subsequent calls to scheduling methods will cause
     * {@link IllegalStateException} to be thrown.
     */
    public synchronized void close()
    {
        if (m_isClosed)
        {
            return;
        }

        m_tasks.clear();
        m_isClosed = true;
        notify();
    }

    /**
     * @see com.kaixin001.tk.api.Scheduler#rescheduleTask(java.lang.Object,
     *      long, long)
     */
    public Object rescheduleTask(Object id, long base, long period)
        throws IllegalArgumentException
    {
        if (!(id instanceof Task))
        {
            throw new IllegalArgumentException(
                MessageFormat.format("Invalid task id {0}", id));
        }

        Task t = (Task) id;

        t.reschedule(base, period);
        return t;
    }

    /**
     * @see java.lang.Thread#run()
     */
    @Override public void run()
    {
        while (true)
        {
            Task task = waitForNextTask();

            if (task == null)
            {
                return;
            }

            try
            {
                task.run();
            }
            catch (Exception iex)
            {
                task.endRun();
            }
        }
    }

    /**
     * @see com.kaixin001.tk.api.Scheduler#scheduleTask(java.lang.Runnable,
     *      long, long, boolean)
     */
    public Object scheduleTask(Runnable task, long base, long period,
            boolean skipConcurrent)
    {
        Task t = new Task(task, skipConcurrent);

        t.reschedule(base, period);
        return t;
    }

    /**
     * @see java.lang.Object#toString()
     */
    public String toString()
    {
        return getName();
    }

    /**
     * Waits for next task to execute.
     * @return task to execute or <code>null</code>, if scheduler is closed
     */
    final synchronized Task waitForNextTask()
    {
        while (true)
        {
            if (m_isClosed)
            {
                return null;
            }

            Task task = m_tasks.peek();

            if (task == null)
            {
                try
                {
                    wait();
                }
                catch (InterruptedException iex)
                {
                }

                continue;
            }

            long delta = task.m_nextRunTime - System.currentTimeMillis();

            if (delta <= 0)
            {
                task = m_tasks.poll();    // to remove t
                task.startRun();
                return task;
            }
            else
            {
                try
                {
                    wait(delta);
                }
                catch (InterruptedException iex)
                {
                }
            }
        }
    }

    /**
     * Removes given task from task queue (if neccessary).<br>
     * Calling thread must hold lock on scheduler.
     * @param t task to remove
     */
    private final void cancelNextRun(Task t)
    {
        m_tasks.remove(t);
    }

    /**
     * Adds given task to the task queue in proper place and notifies worker
     * thread, if neccessary.<br>
     * Calling thread must hold lock on scheduler.
     * @param t task to add
     */
    private final void scheduleNextRun(Task t)
    {
        if (m_isClosed)
        {
            return;
        }

        m_tasks.add(t);
        if (m_tasks.peek() == t)
        {
            notify();
        }
    }

}
